package cn.jtcoding.gateway.auth;

import com.google.common.base.Joiner;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;

/**
 * @author jason.tang
 * @date 2021/4/9 13:41
 * @desc 数字签名工具类
 */

@Slf4j
public class AuthSigner {

    private static final String RESERVED_CHAR_SET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.~_";

    protected final String authPrefix;
    protected final String authHeader;
    protected final long expiredSeconds;
    protected final String secretKey;

    public AuthSigner(String authPrefix, String authHeader, long expiredSeconds, String secretKey) {
        this.authPrefix = authPrefix;
        this.authHeader = authHeader;
        this.expiredSeconds = expiredSeconds;
        this.secretKey = secretKey;
    }

    public String sign(String httpMethod, String uri, Map<String, String> headers, List<String> headersOpt) {
        if (headersOpt == null || headersOpt.isEmpty()) {
            return "";
        }
        String canonicalRequest = this.canonicalRequest(httpMethod, uri, headers, headersOpt);
        String signingKey = this.sha256Hex(secretKey, authPrefix);
        String signature = this.sha256Hex(signingKey, canonicalRequest);
        String result;
        if (!headersOpt.isEmpty()) {
            String signedHeaders = Joiner.on(";").skipNulls().join(headersOpt);
            result = authPrefix + '/' + signedHeaders + '/' + signature;
        } else {
            result = authPrefix + "//" + signature;
        }
        return result;
    }

    public String canonicalRequest(String httpMethod, String uri, Map<String, String> headers, List<String> headersOpt) {
        String canonicalURI = this.canonicalURI(uri);
        String canonicalQueryStr = this.canonicalQueryStr(uri);
        String canonicalHeader = this.canonicalHeaders(headers, headersOpt);
        return Joiner.on("\n").skipNulls().join(httpMethod, canonicalURI, canonicalQueryStr, canonicalHeader);
    }

    public String canonicalHeaders(Map<String, String> headers, List<String> headersOpt) {
        List<String> list = new ArrayList<>();
        if (headersOpt.isEmpty()) {
            list.add("host");
            list.add("content-md5");
            list.add("content-length");
            list.add("content-type");
        } else {
            headersOpt.forEach(header -> list.add(header.toLowerCase()));
        }
        List<String> result = new ArrayList<>();
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String key = entry.getKey();
            if (list.contains(key.toLowerCase()) && !authHeader.equalsIgnoreCase(key)) {
                String value = entry.getValue().trim();
                result.add(this.normalizePath(key.toLowerCase(), true) + ":" + this.normalizePath(value, true));
            }
        }
        Collections.sort(result);
        return Joiner.on("\n").skipNulls().join(result);
    }

    public String canonicalURI(String path) {
        return this.normalizePath(StringUtils.substringBefore(path, "?"), false);
    }

    public String canonicalQueryStr(String uri) {
        String queryStr = StringUtils.substringAfter(uri, "?");
        if (queryStr.isEmpty()) {
            return "";
        }
        String[] queryArr = queryStr.split("&");
        List<String> params = new ArrayList<>();
        for (String str : queryArr) {
            String[] arr = StringUtils.split(str, "=");
            if (arr == null || arr.length < 2 || "token".equalsIgnoreCase(arr[0])
                    || "refreshtoken".equalsIgnoreCase(arr[0])) {
                continue;
            }
            String param = this.normalizeQueryStr(Optional.ofNullable(arr[0]).orElse(""), true) +
                    "=" + this.normalizeQueryStr(Optional.ofNullable(arr[1]).orElse(""), true);
            params.add(param);
        }
        Collections.sort(params);
        return String.join("&", params);
    }

    public String normalizePath(String path, boolean escape) {
        if (path.isEmpty()) {
            return "";
        }
        String result = "";
        Map<String, String> encodeMap = new HashMap<>();
        encodeMap = this.getNormalizedChar(escape);
        for (int i = 0; i < path.length(); i++) {
            if (String.valueOf(path.charAt(i)).equals("/") && !escape) {
                result = result + "/";
            }
            if (String.valueOf(encodeMap.get(String.valueOf(path.charAt(i)))).equals("null")) {
                continue;
            }
            result = result + String.valueOf(encodeMap.get(String.valueOf(path.charAt(i))));
        }
        return result;
    }

    public String normalizeQueryStr(String str, boolean escape) {
        if (str.isEmpty()) {
            return "";
        }
        StringBuilder result = new StringBuilder();
        Map<String, String> encodeMap = new HashMap<>();
        encodeMap = this.getNormalizedChar(escape);
        for (int i = 0; i < str.length(); i++) {
            if (String.valueOf(str.charAt(i)).equals("=") && !escape) {
                result.append("=");
            }
            if (String.valueOf(encodeMap.get(String.valueOf(str.charAt(i)))).equals("null")) {
                continue;
            }
            result.append(encodeMap.get(String.valueOf(str.charAt(i))));
        }
        return result.toString();
    }

    public String sha256Hex(String secret, String message) {
        String hash = "";
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
            mac.init(secretKey);
            byte[] bytes = mac.doFinal(message.getBytes());
            hash = this.byteArrayToHexString(bytes);
        } catch (Exception e) {
            log.error("HmacSHA256 error", e);
        }
        return hash;
    }

    public Map<String, String> getNormalizedChar(boolean escape) {
        HashMap<String, String> result = new HashMap<>();
        for (int i = 0; i < 256; i++) {
            char c = (char) i;
            if (!escape && c == '/') {
                continue;
            }
            if (RESERVED_CHAR_SET.contains(String.valueOf(c))) {
                result.put(String.valueOf(c), String.valueOf(c));
            } else {
                result.put(String.valueOf(c), "%" + String.format("%02X", i));
            }
        }
        return result;
    }

    public String byteArrayToHexString(byte[] b) {
        StringBuilder sb = new StringBuilder();
        String stmp;
        for (int n = 0; b != null && n < b.length; n++) {
            stmp = Integer.toHexString(b[n] & 0XFF);
            if (stmp.length() == 1) {
                sb.append('0');
            }
            sb.append(stmp);
        }
        return sb.toString().toLowerCase();
    }
}
